import { Effect } from '@babylonjs/core/Materials/effect';
import { Mesh } from '@babylonjs/core/Meshes/mesh';
import { Scene } from '@babylonjs/core/scene';
import { VertexData } from '@babylonjs/core/Meshes/mesh.vertexData';
import { Utils } from '../tool/utils';
import { Vector2, Vector3 } from '@babylonjs/core/Maths/math.vector';
import { Random } from '../tool/random';
import { Buffer } from '@babylonjs/core/Buffers/buffer';
import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder';
import { ShaderMaterial } from '@babylonjs/core/Materials/shaderMaterial';
import { Texture } from '@babylonjs/core/Materials/Textures/texture';
import { AnimationFrame } from '../animation/animation-frame';


const vertexShader = `
precision highp float;
attribute vec3 position;
attribute vec2 posRef;
attribute vec2 uv;
attribute float bladeLengthRef;
attribute vec2 uvRef;
attribute vec4 deformRef;
uniform mat4 view;
uniform mat4 world;
uniform mat4 projection;
uniform float time;
uniform float strength;
uniform vec2 direction;
varying vec2 vUV;
varying float fFogDistance;
vec2 rotate(float x,float y,float r){
    float c=cos(r);
    float s=sin(r);
    return vec2(x*c-y*s,x*s+y*c);
}
void main(){
    float invBladePer=1.-bladeLengthRef;
    vec4 p=vec4(position,1.);
    p.xy *=deformRef.x;

    float lean=deformRef.z;
    lean=lean*pow(invBladePer,3.);
    p.z=lean;
    p.y*=sqrt(1.-(lean*lean));
    p.xz=rotate(p.x,p.z,deformRef.y*360.);
    p.xz+=posRef;

    float wind=(sin(p.x+time)+cos(p.z+time))*strength;
    p.xz+=(wind*(invBladePer*invBladePer))*direction;

    vUV=uv;
    fFogDistance=(projection*view*p).z;
    gl_Position=projection*view*world*p;
}
`;

const fragmentShader = `
#define FOGMODE_NONE 0.
#define FOGMODE_EXP 1.
#define FOGMODE_EXP2 2.
#define FOGMODE_LINEAR 3.
#define E 2.71828
precision highp float;
varying vec2 vUV;
uniform sampler2D dTexture;
uniform vec3 multiColor;
uniform vec3 addColor;
uniform float gray;
uniform vec4 vFogInfos;
uniform vec3 vFogColor;
varying float fFogDistance;
float CalcFogFactor(){
    float fogCoeff = 1.0;
    float fogStart = vFogInfos.y;
    float fogEnd = vFogInfos.z;
    float fogDensity = vFogInfos.w;
    if (FOGMODE_LINEAR == vFogInfos.x){
        fogCoeff = (fogEnd - fFogDistance) / (fogEnd - fogStart);
    }else if (FOGMODE_EXP == vFogInfos.x){
        fogCoeff = 1.0 / pow(E, fFogDistance * fogDensity);
    }else if (FOGMODE_EXP2 == vFogInfos.x){
        fogCoeff = 1.0 / pow(E, fFogDistance * fFogDistance * fogDensity * fogDensity);
    }
    return clamp(fogCoeff, 0.0, 1.0);
}
float Gray(float f,float t){
    return gray*f+(1.-gray)*t;
}
void main(){
    vec4 base=texture2D(dTexture,vUV);
    if(base.a<0.5)discard;
    float len=length(base.rgb);
    float fog=CalcFogFactor();
    vec3 fCol=vec3(Gray(len,base.r),Gray(len,base.g),Gray(len,base.b));
    fCol=vec3(fCol.r*multiColor.x+addColor.x,fCol.g*multiColor.y+addColor.y,fCol.b*multiColor.z+addColor.z);
    gl_FragColor=vec4(fog*fCol+(1.-fog)*vFogColor,1.);
}
`;

Effect.ShadersStore['grassFieldVertexShader'] = vertexShader;
Effect.ShadersStore['grassFieldFragmentShader'] = fragmentShader;


/**
 * 草地参数对象
 * @param bladeHeight 长度
 * @param bladeWidth 宽度
 * @param zoneSize 生成范围
 * @param bladeXSegmnets 横向分段数
 * @param bladeYSegmnets 纵向分段数
 * @param density 单位面积生成密度
 * @param texture 纹理
 * @param billCount 片数
 * @param strength 形变强度
 * @param multiColor 颜色（乘法）
 * @param addColor 颜色（加法）
 * @param speed 形变速度
 * @param direction 形变方向
 * @param gray 灰度
 */
interface IGrassFieldOptions {
    bladeHeight?: number,
    bladeWidth?: number,
    zoneSize?: Vector2,
    bladeXSegmnets?: number,
    bladeYSegments?: number,
    density?: number,
    texture?: Texture,
    billCount?: number,
    strength?: number,
    multiColor?: Vector3,
    addColor?: Vector3,
    speed?: number,
    direction?: Vector2,
    gray?: number,
}


/**
 * 草地
 * @param name 名称
 * @param scene 所在场景
 * @param options 草地参数对象
 */
class GrassField {

    private _options: IGrassFieldOptions;
    private _time = 0;
    private _mesh!: Mesh;
    private _material!: ShaderMaterial;
    private _req = {};

    /**
     * 名称
     */
    public readonly name: string;
    /**
     * 唯一id
     */
    public readonly id = Utils.id;
    /**
     * 所在场景
     */
    public readonly scene: Scene;

    /**
     * 获取网格对象
     */
    public get mesh(): Mesh {
        return this._mesh;
    }

    /**
     * 获取纹理
     */
    public get texture(): Texture | undefined {
        return this._options.texture;
    }

    /**
     * 设置纹理
     */
    public set texture(texture: Texture) {
        this._options.texture = texture;
        if (this._options.texture) this._material.setTexture('dTexture', this._options.texture);
    }

    /**
     * 获取颜色（乘）
     */
    public get multiColor(): Vector3 {
        return this._options.multiColor!;
    }

    /**
     * 设置颜色（乘）
     */
    public set multiColor(color: Vector3) {
        this._options.multiColor!.copyFrom(color);
        this._material.setVector3('multiColor', this._options.multiColor!);
    }

    /**
     * 获取颜色（加）
     */
    public get addColor(): Vector3 {
        return this._options.addColor!;
    }

    /**
     * 设置颜色（加）
     */
    public set addColor(color: Vector3) {
        this._options.addColor!.copyFrom(color);
        this._material.setVector3('addColor', this._options.addColor!);
    }

    /**
     * 获取形变方向
     */
    public get direction(): Vector2 {
        return this._options.direction!;
    }

    /**
     * 设置形变方向
     */
    public set direction(direction: Vector2) {
        this._options.direction!.copyFrom(direction);
        this._material.setVector2('direction', this._options.direction!);
    }

    /**
     * 获取形变强度
     */
    public get strength(): number {
        return this._options.strength!;
    }

    /**
     * 设置形变强度
     */
    public set strength(strength: number) {
        this._options.strength = strength;
        this._material.setFloat('strength', this._options.strength!);
    }

    /**
     * 获取灰度
     */
    public get gray(): number {
        return this._options.gray!;
    }

    /**
     * 设置灰度
     */
    public set gray(gray: number) {
        this._options.gray = gray;
        this._material.setFloat('gray', this._options.gray!);
    }

    /**
     * 获取形变速度
     */
    public get speed(): number {
        return this._options.speed!;
    }

    /**
     * 设置形变速度
     */
    public set speed(speed: number) {
        this._options.speed = speed;
    }

    /**
     * 销毁
     */
    public dispose(): void {
        AnimationFrame.cancel(this._req);
        this._mesh.dispose(false, true);
    }

    constructor(name: string, scene: Scene, options?: IGrassFieldOptions) {
        this.name = name;
        this.scene = scene;
        this._options = this.initializeOptions(options);
        this.initialize();
    }

    private initializeOptions(ops?: IGrassFieldOptions): IGrassFieldOptions {
        const o = ops || {};
        return {
            bladeHeight: o.bladeHeight || 1,
            bladeWidth: o.bladeWidth || 1,
            zoneSize: o.zoneSize || new Vector2(10, 10),
            bladeXSegmnets: o.bladeXSegmnets || 1,
            bladeYSegments: o.bladeYSegments || 1,
            density: o.density || 0.2,
            texture: o.texture,
            billCount: o.billCount || 1,
            multiColor: o.multiColor || new Vector3(1, 1, 1),
            addColor: o.addColor || new Vector3(0, 0, 0),
            strength: o.strength === undefined ? 0.1 : o.strength,
            speed: o.speed === undefined ? 0.001 : o.speed,
            direction: o.direction || new Vector2(0.5, 0.5).normalize(),
            gray: o.gray || 0,
        };
    }

    private initialize(): void {
        const baseMesh = this.createBaseMesh();
        this.distribute(baseMesh);
        this.animate();
    }

    private distribute(mesh: Mesh): void {
        const scene = this.scene;
        mesh.setEnabled(false);
        const meshes: Mesh[] = [];
        const density = this._options.density!;
        const engine = scene.getEngine();

        const uvRef: number[] = [];
        const deformRef: number[] = [];
        const posRef: number[] = [];
        const bladeLengthRef: number[] = [];
        const gridX = this._options.bladeXSegmnets!;
        const gridY = this._options.bladeYSegments!;

        const mx = this._options.zoneSize!.x;
        const my = this._options.zoneSize!.y;

        let x = 0, y = 0, xp = 0, yp = 0, counter = 0, scale = 0, rotate = 0, curve = 0;
        const seg = this._options.billCount!;
        for (y = 0; y < my; y += density + Math.abs(Math.pow((y - my / 2) / (my / 2), 2) + Math.pow((x - mx / 2) / (mx / 2), 2)) * density * 20) {
            for (x = 0; x < mx; x += density + Math.abs(Math.pow((y - my / 2) / (my / 2), 2) + Math.pow((x - mx / 2) / (mx / 2), 2)) * density * 20) {
                counter++;
                counter %= seg;
                if (!counter) {
                    xp = x - (mx * 0.5) + Math.random() * density * 20;
                    yp = y - (my * 0.5) + Math.random() * density * 20;
                    scale = Random.float(0.5, 1.5);
                    rotate = Random.float(0, 1);
                    curve = Random.float(0.2, 0.8);
                } else {
                    rotate += 1 / seg;
                    rotate %= 1;
                }
                const temp = mesh.clone('temp');

                for (let segY = 0; segY <= gridY; segY++) {
                    let bladePer = 0;
                    if (segY > 0) bladePer = segY / gridY;
                    for (let segX = 0; segX <= gridX; segX++) {
                        posRef.push(xp, yp);
                        uvRef.push(x / mx, y / my);
                        deformRef.push(scale, rotate, curve, 0);
                        bladeLengthRef.push(bladePer)
                    }
                }

                meshes.push(temp);
            }
        }

        mesh.dispose();
        mesh = Mesh.MergeMeshes(meshes, true, true)!;

        const posRefBuffer = new Buffer(engine, posRef, false, 2);
        mesh.setVerticesBuffer(posRefBuffer.createVertexBuffer("posRef", 0, 2));

        const uvRefBuffer = new Buffer(engine, uvRef, false, 2);
        mesh.setVerticesBuffer(uvRefBuffer.createVertexBuffer("uvRef", 0, 2));

        const bladeLengthRefBuffer = new Buffer(engine, bladeLengthRef, false, 1);
        mesh.setVerticesBuffer(bladeLengthRefBuffer.createVertexBuffer("bladeLengthRef", 0, 1));

        const deformRefBuffer = new Buffer(engine, deformRef, false, 4);
        mesh.setVerticesBuffer(deformRefBuffer.createVertexBuffer("deformRef", 0, 4));

        const grassMat = new ShaderMaterial(this.name + "-material", scene, {
            vertexElement: "grassField",
            fragmentElement: "grassField",
        }, {
            attributes: ["position", "uv", "posRef", "bladeLengthRef", "uvRef", "deformRef"],
            samplers: ['dTexture'],
            uniforms: ["world", "view", "projection", "time", "multiColor", "addColor", "strength", "direction", "vFogInfos", "vFogColor", "gray"],
        });
        grassMat.onBind = () => {
            const efc = grassMat.getEffect();
            efc.setFloat4('vFogInfos', scene.fogMode, scene.fogStart, scene.fogEnd, scene.fogDensity);
            efc.setColor3('vFogColor', scene.fogColor);
        };

        this._time = 0;
        if (this._options.texture) grassMat.setTexture('dTexture', this._options.texture);
        grassMat.setVector3('multiColor', this._options.multiColor!);
        grassMat.setVector3('addColor', this._options.addColor!);
        grassMat.setFloat('strength', this._options.strength!);
        grassMat.setFloat('gray', this._options.gray!);
        grassMat.setVector2('direction', this._options.direction!);
        mesh.material = grassMat;

        const _bb = MeshBuilder.CreateBox('tempBox', { size: 1 }, scene);
        _bb.scaling = new Vector3(this._options.zoneSize!.x, this._options.bladeHeight! * 2.0, this._options.zoneSize!.y);
        _bb.position.y = (this._options.bladeHeight! * 2.0) * 0.5;
        _bb.bakeCurrentTransformIntoVertices();
        mesh.setBoundingInfo(_bb.getBoundingInfo());
        _bb.dispose();
        this._mesh = mesh;
        this._material = grassMat;
    }

    private createBaseMesh(): Mesh {
        const args = this._options;
        const scene = this.scene;
        const width = args.bladeWidth!;
        const height = args.bladeHeight!;
        const width_half = width / 2;
        const height_half = height / 2;
        const gridX = args.bladeXSegmnets!;
        const gridY = args.bladeYSegments!;
        const gridX1 = gridX + 1;
        const gridY1 = gridY + 1;
        const segment_width = width / gridX;
        const segment_height = height / gridY;

        const vDat = new VertexData();
        const indices: number[] = vDat.indices = [];
        const positions: number[] = vDat.positions = [];
        const normals: number[] = vDat.normals = [];
        const uvs: number[] = vDat.uvs = [];

        for (let iy = 0; iy < gridY1; iy++) {
            const y = iy * segment_height - height_half;
            for (let ix = 0; ix < gridX1; ix++) {
                const x = ix * segment_width - width_half;
                positions.push(x, - y, 0);
                normals.push(0, 0, 1);
                uvs.push(ix / gridX, 1 - (iy / gridY));
            }
        }

        for (let iy = 0; iy < gridY; iy++) {
            for (let ix = 0; ix < gridX; ix++) {
                const a = ix + gridX1 * iy;
                const b = ix + gridX1 * (iy + 1);
                const c = (ix + 1) + gridX1 * (iy + 1);
                const d = (ix + 1) + gridX1 * iy;
                indices.push(a, b, d, b, c, d, d, c, b, d, b, a);
            }
        }

        const mesh = new Mesh(this.name + '-base-mesh', scene);
        vDat.applyToMesh(mesh);
        mesh.position.y = height_half;
        mesh.bakeCurrentTransformIntoVertices();
        return mesh;
    }

    private animate(): void {
        const grassMat = this._material;
        AnimationFrame.request((t) => {
            grassMat.setFloat('time', this._time);
            this._time += t * this._options.speed!;
        }, 16, 200, this._req);
    }

}


export { GrassField };
export type { IGrassFieldOptions };